/**
 * Copyright © 2011,2013 Konstantin Livitski
 * 
 * This file is part of n-Puzzle application. n-Puzzle application is free
 * software; you can redistribute it and/or modify it under the terms of the GNU
 * General Public License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 * 
 * n-Puzzle application contains adaptations of artwork covered by the Creative
 * Commons Attribution-ShareAlike 3.0 Unported license. Please refer to the
 * NOTICE.md file at the root of this distribution or repository for licensing
 * terms that apply to that artwork.
 * 
 * n-Puzzle application is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * n-Puzzle application; if not, see the LICENSE/gpl.txt file of this distribution
 * or visit <http://www.gnu.org/licenses>.
 */
package name.livitski.games.puzzle.android;

import java.io.Serializable;
import java.util.Map;

import name.livitski.games.puzzle.android.model.Board;
import name.livitski.games.puzzle.android.model.Direction;
import name.livitski.games.puzzle.android.model.Game;
import name.livitski.games.puzzle.android.model.Game.Level;
import name.livitski.games.puzzle.android.model.ImageProcessingException;
import name.livitski.games.puzzle.android.model.MoveListener;
import name.livitski.games.puzzle.android.model.Tile;
import android.app.Dialog;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.GestureDetector;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TableLayout;
import android.widget.TableRow;

/**
 * User's interface to the puzzle.
 */
public class GamePlay extends Activity implements MoveListener {

	private GestureDetector gestureScanner;

	private Game game;
	private RelativeLayout windowLayout;
	private TableLayout imageBoardView;
	private ImageView[][] imageCells;
	private boolean dialogActive;
	private int lastWidth = -1, lastHeight = -1;

	private static final int IMAGE_SELECTION_REQUEST_CODE = 1;
	private static final int CONGRATULATIONS_REQUEST_CODE = 2;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		initLayout();
		initGame();
		initBoard();
		gestureScanner = new GestureDetector(this, simpleOnGestureListener);
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		MenuInflater inflater = getMenuInflater();
		inflater.inflate(R.menu.game_menu, menu);
		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		switch (item.getItemId()) {
		case R.id.item_reshuffle:
			restart(false);
			break;
		case R.id.item_change_picture:
			restart(true);
			break;
		case R.id.item_difficulty_hard:
			if (game.getDifficulty() != Game.Level.HARD)
				restart(Game.Level.HARD);
			break;
		case R.id.item_difficulty_medium:
			if (game.getDifficulty() != Game.Level.MEDIUM)
				restart(Game.Level.MEDIUM);
			break;
		case R.id.item_difficulty_easy:
			if (game.getDifficulty() != Game.Level.EASY)
				restart(Game.Level.EASY);
			break;
		default:
			return super.onOptionsItemSelected(item);
		}
		resumeImpl();
		return true;
	}

	/**
	 * Updates the view when the model posts a tile move notification.
	 */
	public void tileMoved(Tile tile) {
		game.initTileImage(this, tile);
	}

	@Override
	protected void onResume() {
		super.onResume();
		resumeImpl();
	}

	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		switch (requestCode) {
		case IMAGE_SELECTION_REQUEST_CODE:
			if (RESULT_CANCELED == resultCode)
				finish();
			else {
				Serializable id = data.getSerializableExtra(ImageSelection.EXTRA_SELECTED_IMAGE_ID_KEY);
				if (null == id)
					throw new IllegalStateException("Image selection page did not return an image");
				Level requestedLevel = (Level) data
						.getSerializableExtra(ImageSelection.EXTRA_SELECTED_IMAGE_INITIAL_LEVEL);
				if (null != requestedLevel && requestedLevel != game.getDifficulty())
					restart(requestedLevel);
			}
			break;
		case CONGRATULATIONS_REQUEST_CODE:
			if (RESULT_OK != resultCode)
				finish();
			break;
		default:
			throw new IllegalStateException("Received a result from unknown activity, code = " + requestCode);
		}
	}

	@Override
	protected void onPause() {
		super.onPause();
		saveSettings();
	}

	/** Returns the current board size. */
	public int getBoardSize() {
		return getBoardModel().getSize();
	}

	/**
	 * Discards the current board state and starts a new game. Unless you are
	 * switching activities, you have to follow up this method with a call to
	 * {@link #resumeImpl()} to show the new board.
	 * 
	 * @param selectNewImage
	 *            tells the puzzle whether the user would like to select a new
	 *            image
	 */
	protected void restart(boolean selectNewImage) {
		hideBoard();
		if (selectNewImage)
			newGame(null);
		else
			game.start();
		initBoard();
	}

	/**
	 * Starts a new game with a specific difficulty level. If there is an image
	 * selected for the current game, uses the same image for the new game.
	 * Unless you are switching activities, you have to follow up this method
	 * with a call to {@link #resumeImpl()} to show the new board.
	 * 
	 * @param difficulty
	 *            difficulty level for the new game
	 */
	protected void restart(Game.Level difficulty) {
		hideBoard();
		if (null != game)
			newGame(difficulty);
	}

	protected void hideBoard() {
		windowLayout.removeAllViews();
		windowLayout.invalidate();
	}

	protected void showImageBoard() {
		// force resizeContent() if the board was hidden since onMeasure()
		// events might have been missed
		if (0 == windowLayout.getChildCount())
			lastWidth = lastHeight = -1;
		windowLayout.removeAllViews();
		windowLayout.addView(imageBoardView);
		windowLayout.invalidate();
	}

	/**
	 * Toggles the "cheat mode" that shows the tiles' numbers.
	 */
	protected void toggleBoardType() {
		if (0 == windowLayout.getChildCount())
			return;
		else
			showImageBoard();
	}

	protected Board getBoardModel() {
		return game.getBoard();
	}

	protected void initLayout() {
		windowLayout = new RelativeLayout(this) {
			@Override
			protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
				int width = MeasureSpec.getSize(widthMeasureSpec);
				int height = MeasureSpec.getSize(heightMeasureSpec);
				if (width != lastWidth || height != lastHeight) {
					// Log.d(
					// getClass().getName(),
					// "resizing to width = " +
					// Integer.toHexString(widthMeasureSpec) + " ("
					// + width + ") " + ", height = "
					// + Integer.toHexString(heightMeasureSpec) + " ("
					// + height + ")");
					if (0 < getChildCount())
						resizeContent(width, height);
					lastWidth = width;
					lastHeight = height;
				}
				super.onMeasure(widthMeasureSpec, heightMeasureSpec);
			}
		};
		windowLayout.setBackgroundColor(getResources().getColor(R.color.background));
		setContentView(windowLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
				ViewGroup.LayoutParams.MATCH_PARENT));
	}

	protected void initGame() {
		SharedPreferences preferences = getPreferences(MODE_PRIVATE);
		Map<String, ?> settings = preferences.getAll();
		try {
			if (settings.containsKey(Game.DIFFICULTY_SETTING))
				newGame(Game.Level.valueOf((String) settings.get(Game.DIFFICULTY_SETTING)));
			else
				newGame(null);
		} catch (RuntimeException invalid) {
			Log.w(getClass().getName(), "Error loading settings", invalid);
			newGame(null);
		}
		game.load(preferences);
	}

	protected void newGame(Level difficulty) {
		if (null != difficulty)
			game = new Game(difficulty);
		else if (null == game)
			game = new Game();
		else
			game = new Game(game.getDifficulty());
	}

	protected void initBoard() {
		imageBoardView = new TableLayout(this);
		final Board board = getBoardModel();
		board.addMoveListener(this);
		final int boardSize = getBoardSize();
		imageCells = new ImageView[boardSize][boardSize];
		for (int i = 0; i < boardSize; i++) {
			TableRow imageRow = new TableRow(this);
			for (int j = 0; j < boardSize; j++) {
				ImageView imageCell = new ImageView(this);
				imageCells[i][j] = imageCell;
				imageRow.addView(imageCell);
			}
			imageBoardView.addView(imageRow);
		}
		game.randomNumber();
	}

	protected void resumeImpl() {
		if (dialogActive)
			hideBoard();
		else {
			showImageBoard();
			if (!game.isStarted())
				showPreview();
		}
	}

	protected void saveSettings() {
		Editor settings = getPreferences(MODE_PRIVATE).edit();
		game.save(settings);
		settings.commit();
	}

	protected void congratulate() {
		Intent intent = new Intent(this, YouWin.class);
		intent.putExtra(YouWin.EXTRA_MOVE_COUNT, game.getMoveCount());
		startActivityForResult(intent, CONGRATULATIONS_REQUEST_CODE);
		restart(true);
	}

	protected void showPreview() {
		game.preview();
		int lastIndex = getBoardSize() - 1;
		final View blankTileCell = imageCells[lastIndex][lastIndex];
		final ViewGroup parent = (ViewGroup) blankTileCell.getParent();
		parent.removeView(blankTileCell);
		hideBoard();
		parent.addView(blankTileCell);
		game.start();
		showImageBoard();

	}

	protected void wrongClick() {
	}

	protected void resizeContent(final int screenWidth, final int screenHeight) {
		if (null == game)
			throw new IllegalStateException(this + " must be initialized with onCreate()");
		final float imageRatio = game.getImageAspectRatio();
		final int boardSize = getBoardSize();
		final DisplayMetrics metrics = getResources().getDisplayMetrics();
		// border width 1 dp rounded up to nearest whole pixels
		final int borderWidth = (int) Math.ceil(metrics.density);
		// calculate spacing allotment
		final int spacing = borderWidth * 2 * boardSize;
		if (screenWidth < spacing + boardSize || screenHeight < spacing + boardSize)
			throw new UnsupportedOperationException("Screen size (" + screenWidth + " x " + screenHeight
					+ ") too small for a board of " + boardSize + " rows");
		final float adjustedScreenRatio = (float) (screenWidth - spacing) / (screenHeight - spacing);

		int width, height;
		if (adjustedScreenRatio > imageRatio) {
			// scale to screen height
			height = screenHeight;
			// fix width = imageRatio * height
			width = (int) (imageRatio * height);
			if (width < spacing + boardSize)
				throw new UnsupportedOperationException("Need a wider image to make a board: scaled to " + width
						+ " pixels, need " + (spacing + boardSize));
		} else {
			// scale to screen width
			width = screenWidth;
			// fix height = width / imageRatio
			height = (int) (width / imageRatio);
			if (height < spacing + boardSize)
				throw new UnsupportedOperationException("Need a taller image to make a board: scaled to " + height
						+ " pixels, need " + (spacing + boardSize));
		}

		// make the dimensions divisible by row/column count
		height -= height % boardSize;
		width -= width % boardSize;
		RelativeLayout.LayoutParams boardLayoutParams = new RelativeLayout.LayoutParams(width, height);
		boardLayoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
		imageBoardView.setLayoutParams(boardLayoutParams);

		// load and resize the image
		width = width / boardSize - 2 * borderWidth;
		height = height / boardSize - 2 * borderWidth;
		game.setTileSize(width, height);
		try {
			game.loadImage(this);
		} catch (ImageProcessingException failure) {
			Log.e(getClass().getName(), failure.getMessage(), failure);
			hideBoard();
			alert(R.string.image_load_error);
			return;
		}

		// size and fill cell views
		TableRow.LayoutParams cellParams = new TableRow.LayoutParams(width, height);
		cellParams.setMargins(borderWidth, borderWidth, borderWidth, borderWidth);
		float fontSize = width * 4f / 3;
		if (fontSize > height)
			fontSize = height;
		fontSize *= .5f;
		final Board board = getBoardModel();
		for (int i = 0; i < boardSize; i++) {
			for (int j = 0; j < boardSize; j++) {
				ImageView imageCell = imageCells[i][j];
				imageCell.setLayoutParams(cellParams);
				Tile tile = board.getTileAt(i, j);
				assignTile(imageCell, tile);
			}
		}
	}

	@Override
	protected void onPrepareDialog(int id, Dialog dialog) {
		super.onPrepareDialog(id, dialog);
		dialogActive = true;
	}

	@Override
	protected void onDialogResponse(int dialogId, int response) {
		dialogActive = false;
		resumeImpl();
	}

	@Override
	protected void onDialogCancel(int dialogId) {
		if (dialogActive) {
			dialogActive = false;
			resumeImpl();
		}
	}

	protected void alert(int messageId) {
		String msg = getResources().getString(messageId);
		alert(msg);
	}

	private void assignTile(final ImageView cell, final Tile tile) {
		cell.setImageDrawable(tile.getDrawable());
		cell.setTag(tile);
	}

	private void onSwipe(int direction) {
		final Board board = getBoardModel();
		final boolean move = board.move(direction);
		if (move) {
			if (game.isSolved())
				congratulate();
		} else {
			wrongClick();
		}

	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d(null, "Touch");
		if (gestureScanner.onTouchEvent(event))
			return true;
		else
			return super.onTouchEvent(event); // or false (it's what you whant).
	}

	GestureDetector.SimpleOnGestureListener simpleOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
		@Override
		public boolean onDown(MotionEvent event) {
			return true;
		}

		@Override
		public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) {
			Log.d(null, "Fling");
			if (Math.abs(velocityX) > Math.abs(velocityY)) {
				int dx = (int) (event2.getX() - event1.getX());
				// don't accept the fling if it's too short
				// as it may conflict with a button push
				if (Math.abs(dx) > 50) {
					if (velocityX > 0) {
						onSwipe(Direction.RIGHT);
					} else {
						onSwipe(Direction.LEFT);
					}
					return true;
				} else {
					return false;
				}
			} else {
				int dy = (int) (event2.getY() - event1.getY());
				// don't accept the fling if it's too short
				// as it may conflict with a button push
				if (Math.abs(dy) > 50) {
					if (velocityY > 0) {
						onSwipe(Direction.UP);
					} else {
						onSwipe(Direction.DOWN);
					}
					return true;
				} else {
					return false;
				}
			}

		}
	};
}